package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"os/signal"
	"path/filepath"
	"runtime"
	"strings"
	"syscall"
	"time"
)

const (
	// AppName 表示应用程序名称。
	AppName string = "lancer"
	// AppVersion 表示应用程序版本。
	AppVersion string = "0.3.4"
	// DefaultConfigFile 表示配置文件名。
	DefaultConfigFile string = AppName + ".json"
	// DefaultListenAddress 表示默认的监听地址。
	DefaultListenAddress string = ":5001"
	// DefaultSSLCert 表示默认的SSL证书文件。
	DefaultSSLCert string = "ssl.crt"
	// DefaultSSLCertKey 表示默认的SSL证书私钥文件。
	DefaultSSLCertKey string = "ssl.key"
	// DefaultDebug 表示默认是否开启Debug模式。
	DefaultDebug bool = false
	// DefaultTimeout 表示默认的请求超时秒数。
	DefaultTimeout int = 30
	// DefaultDocument 表示默认的文档名。
	DefaultDocument string = "index.html,index.htm,index"
	// DefaultDocumentExt 表示默认的文档扩展名。
	DefaultDocumentExt string = ".html,.htm"
)

var (
	// flagVersion 是否显示版权信息
	flagVersion bool
	// flagHelp 是否显示帮助信息
	flagHelp bool
	// flagVerify 是否只检查配置文件
	flagVerify bool
	// flagDebug 是否显示调试信息
	flagDebug bool

	configuration *Configuration = &Configuration{
		Listen:  DefaultListenAddress,
		Timeout: DefaultTimeout,
	}

	allMimeTypes map[string]string = map[string]string{
		".txt":  "text/plain;charset=utf-8",
		".text": "text/plain;charset=utf-8",
		".css":  "text/css;charset=utf-8",
		".gif":  "image/gif",
		".htm":  "text/html;charset=utf-8",
		".html": "text/html;charset=utf-8",
		".ico":  "image/icon",
		".icon": "image/icon",
		".jpg":  "image/jpeg",
		".jpeg": "image/jpeg",
		".js":   "application/javascript;charset=utf-8",
		".wasm": "application/wasm",
		".pdf":  "application/pdf",
		".png":  "image/png",
		".svg":  "image/svg+xml",
		".xml":  "text/xml;charset=utf-8",
	}

	defaultDocuments    []string
	defaultDocumentExts []string
)

func init() {
	flag.BoolVar(&flagVersion, "version", false, "Shows version message")
	flag.BoolVar(&flagHelp, "h", false, "Shows this help message")
	flag.BoolVar(&flagVerify, "verify", false, "Verify configuration and quit")
	flag.BoolVar(&flagDebug, "debug", DefaultDebug, "Show debugging information")

	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "Usage: %s -version\n", AppName)
		fmt.Fprintf(os.Stderr, "Usage: %s -h\n", AppName)
		fmt.Fprintf(os.Stderr, "Usage: %s -verify\n", AppName)
		fmt.Fprintf(os.Stderr, "Usage: %s [-debug] [CONFIG_FILE]\n", AppName)
		flag.PrintDefaults()
	}

	defaultDocuments = strings.Split(DefaultDocument, ",")
	defaultDocumentExts = strings.Split(DefaultDocumentExt, ",")
}

func main() {
	flag.Parse()

	if flagVersion {
		fmt.Printf("%s version %s\n", AppName, AppVersion)
		return
	}

	if flagHelp {
		flag.Usage()
		return
	}

	if !flagDebug {
		log.SetOutput(io.Discard)
	}

	// 加载配置。
	if err := loadConfig(strings.TrimSpace(flag.Arg(0))); err != nil {
		fmt.Fprintf(os.Stderr, "Cannot load configuration\n")
		panic(err)
	}

	if flagVerify {
		fmt.Printf("Configuration:\n%s\n", configuration)
		return
	}

	// 输出pid文件。
	pidFilename := AppName + "-pid"
	if runtime.GOOS != "windows" {
		pidFilename = "/var/run/" + pidFilename
	}
	if pidFile, err := os.Create(pidFilename); err == nil {
		pidFile.WriteString(fmt.Sprintf("%v", os.Getpid()))
		pidFile.Close()

		defer os.Remove(pidFilename)
	}

	// 开始服务。
	err := serveForEver()
	if err != nil {
		panic(err)
	}
}

func serveForEver() error {
	var server1, server2 *http.Server
	var err error

	// 依次启动
	if configuration.SSLListen != "" {
		server1, err = initMainServer(configuration.SSLListen)
		if err != nil {
			return err
		}
		go doServeTLS(server1)

		server2, err = initMinorServer(configuration.Listen, configuration.SSLListen)
	} else {
		server2, err = initMainServer(configuration.Listen)
	}

	if err != nil {
		return err
	}
	go doServe(server2)

	// 启动守护routine。
	sigChannel := make(chan os.Signal, 256)
	signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
	for {
		sig := <-sigChannel
		fmt.Fprintf(os.Stderr, "Received sig: %#v\n", sig)
		switch sig {
		case syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM:
			return nil
		}
	}
}

func initMainServer(address string) (*http.Server, error) {
	mux := http.NewServeMux()

	if err := initPing(mux); err != nil {
		return nil, err
	}

	if err := initStatic(mux); err != nil {
		return nil, err
	}

	if err := initProxies(mux); err != nil {
		return nil, err
	}

	return newServer(address, mux), nil
}

func initMinorServer(address0, address1 string) (*http.Server, error) {
	return newServer(address0, &redirectSslHandler{address: address1}), nil
}

func newServer(address string, handler http.Handler) *http.Server {
	return &http.Server{
		Addr:           address,
		Handler:        &shortDomainHandler{handler},
		ReadTimeout:    time.Duration(configuration.Timeout) * time.Second,
		WriteTimeout:   time.Duration(configuration.Timeout) * time.Second,
		MaxHeaderBytes: 2 * 1024,
	}
}

func initPing(mux *http.ServeMux) error {
	mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain")
		w.Header().Set("Content-Length", "4")
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("pong"))
	})

	return nil
}

func doServe(server *http.Server) {
	fmt.Printf("Serving @ %s\n", server.Addr)
	panic(server.ListenAndServe())
}

func doServeTLS(server *http.Server) {
	fmt.Printf("Serving @ %s\n", server.Addr)
	panic(server.ListenAndServeTLS(configuration.SSLCert, configuration.SSLCertKey))
}

type redirectSslHandler struct {
	address string
}

func (rsh *redirectSslHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	newHost := r.Host
	if rsh.address != ":https" && rsh.address != ":443" {
		if rsh.address == ":http" {
			newHost = newHost + ":80"
		} else {
			newHost = newHost + rsh.address
		}
	}
	newURL := url.URL{Scheme: "https", Opaque: r.URL.Opaque, User: r.URL.User,
		Host: newHost,
		Path: r.URL.Path, RawPath: r.URL.RawPath, ForceQuery: r.URL.ForceQuery,
		Fragment: r.URL.Fragment, RawFragment: r.URL.RawFragment}
	w.Header().Set("Strict-Transport-Security", "max-age=31536000;")
	http.Redirect(w, r, newURL.String(), http.StatusTemporaryRedirect)
}

type shortDomainHandler struct {
	handler http.Handler
}

const (
	prefixWWW = "www."
)

func removePrefixWWW(s string) (string, bool) {
	pl := len(prefixWWW)
	flag := len(s) >= pl && strings.ToLower(s[:pl]) == prefixWWW
	if flag {
		return s[pl:], true
	}

	return s, false
}

func (sdh *shortDomainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if newHost, replaced := removePrefixWWW(r.Host); replaced {
		// 去掉域名中的www.前缀。
		newURL := url.URL{Scheme: r.URL.Scheme, Opaque: r.URL.Opaque, User: r.URL.User,
			Host: newHost,
			Path: r.URL.Path, RawPath: r.URL.RawPath, ForceQuery: r.URL.ForceQuery,
			Fragment: r.URL.Fragment, RawFragment: r.URL.RawFragment}
		http.Redirect(w, r, newURL.String(), http.StatusPermanentRedirect)
	} else {
		sdh.handler.ServeHTTP(w, r)
	}
}

func loadConfig(configFile string) (err error) {
	if configFile == "" {
		configFile = DefaultConfigFile
	}

	configFile, err = filepath.Abs(configFile)
	if err != nil {
		return err
	}

	configFileStat, err := os.Stat(configFile)
	if err != nil {
		return err
	}

	if configFileStat.IsDir() {
		configFile = filepath.Join(configFile, DefaultConfigFile)
	}

	// 根据配置文件的基准路径计算完整路径。
	toAbsPath := func(p string) string {
		if filepath.IsAbs(p) {
			return p
		}

		return filepath.Clean(filepath.Join(filepath.Dir(configFile), p))
	}

	err = loadConfigFromFile(configFile)
	if err != nil {
		return err
	}

	// 检查服务绑定地址的格式是否正确。
	configuration.Listen = strings.ToLower(strings.TrimSpace(configuration.Listen))
	configuration.SSLListen = strings.ToLower(strings.TrimSpace(configuration.SSLListen))
	configuration.SSLCert = strings.TrimSpace(configuration.SSLCert)
	configuration.SSLCertKey = strings.TrimSpace(configuration.SSLCertKey)
	if configuration.Listen == "" || configuration.Listen == ":" {
		configuration.Listen = DefaultListenAddress
	} else if !strings.HasPrefix(configuration.Listen, ":") {
		return fmt.Errorf("listen address should start with colon(:), do you prefer %v ?", ":"+configuration.Listen)
	}
	if configuration.SSLListen == "" || configuration.SSLListen == ":" {
		configuration.SSLListen = ""
	} else if !strings.HasPrefix(configuration.SSLListen, ":") {
		return fmt.Errorf("SSL listen address should start with colon(:), do you prefer %v ?", ":"+configuration.SSLListen)
	} else {
		if configuration.SSLCert == "" {
			configuration.SSLCert = toAbsPath(DefaultSSLCert)
		} else {
			configuration.SSLCert = toAbsPath(configuration.SSLCert)
		}
		if configuration.SSLCertKey == "" {
			configuration.SSLCertKey = toAbsPath(DefaultSSLCertKey)
		} else {
			configuration.SSLCertKey = toAbsPath(configuration.SSLCertKey)
		}
	}

	return err
}

func loadConfigFromFile(configFile string) (err error) {
	var cf *os.File

	fmt.Printf("Load configuration from %s\n", configFile)

	if cf, err = os.Open(configFile); err != nil {
		return err
	}

	defer cf.Close()

	dec := json.NewDecoder(cf)
	err = dec.Decode(&configuration)
	if err != nil {
		return err
	}

	configDir := filepath.Dir(configFile)

	// 如果指定的内容根目录不是绝对路径，那么使用配置文件所在的目录计算绝对路径。
	if !filepath.IsAbs(configuration.Base) {
		configuration.Base = filepath.Clean(filepath.Join(configDir, configuration.Base))
	}

	return nil
}
